using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.CodeAnalysis;
using NetPad.Application;
using NetPad.Common;
using NetPad.DotNet;
using NetPad.DotNet.References;
using JsonSerializer = NetPad.Common.JsonSerializer;

namespace NetPad.Scripts;

/// <summary>
/// Represents a deterministic fingerprint of a script’s configuration and content,
/// used to detect when a script or its dependencies have changed and require rebuilding.
/// </summary>
public record ScriptFingerprint(
    string AppVersion,
    string CodeHash,
    string NamespacesHash,
    string ReferencesHash,
    ScriptKind Kind,
    DotNetFrameworkVersion TargetFrameworkVersion,
    OptimizationLevel OptimizationLevel,
    bool UseAspNet,
    Guid? DataConnectionId
)
{
    private static readonly JsonSerializerOptions _serializerOptions = new()
    {
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
    };

    /// <summary>
    /// Calculates a deterministic SHA-256 hash representing the current fingerprint instance.
    /// </summary>
    /// <returns>
    /// A 64-character lowercase hexadecimal string containing the SHA-256 hash of this
    /// <see cref="ScriptFingerprint"/> instance's serialized JSON representation.
    /// </returns>
    /// <remarks>
    /// This value uniquely identifies the fingerprint’s contents and changes whenever any of its
    /// properties differ.
    /// </remarks>
    public string CalculateHash()
    {
        var json = JsonSerializer.Serialize(this, _serializerOptions);
        return Sha256Hex(json);
    }


    /// <summary>
    /// Calculates a deterministic UUID (version 5) representing the current fingerprint instance.
    /// </summary>
    /// <returns>
    /// A <see cref="Guid"/> generated by hashing the fingerprint’s serialized JSON representation
    /// with the application’s fixed UUIDv5 namespace.
    /// </returns>
    /// <remarks>
    /// The returned GUID is a stable identifier for the same fingerprint content, but not
    /// cryptographically unique. It is derived using the <see cref="Uuid5.Create(string)"/> method.
    /// </remarks>
    public Guid CalculateUuid()
    {
        var json = JsonSerializer.Serialize(this, _serializerOptions);
        return Uuid5.Create(json);
    }

    /// <summary>
    /// Calculates a deterministic SHA-256 hash for the specified <see cref="Script"/> instance.
    /// </summary>
    /// <param name="script">The script from which to compute the fingerprint hash.</param>
    /// <returns>
    /// A 64-character lowercase hexadecimal string representing the SHA-256 hash of the
    /// fingerprint derived from the specified script.
    /// </returns>
    public static string CalculateHash(Script script) => Create(script).CalculateHash();

    /// <summary>
    /// Calculates a deterministic UUID (version 5) for the specified <see cref="Script"/> instance.
    /// </summary>
    /// <param name="script">The script from which to compute the fingerprint UUID.</param>
    /// <returns>
    /// A <see cref="Guid"/> that deterministically represents the fingerprint of the specified script,
    /// derived using the UUIDv5 algorithm.
    /// </returns>
    public static Guid CalculateUuid(Script script) => Create(script).CalculateUuid();


    /// <summary>
    /// Creates a new <see cref="ScriptFingerprint"/> instance from the specified
    /// <see cref="Script"/> instance.
    /// </summary>
    /// <param name="script">
    /// The <see cref="Script"/> from which to generate the fingerprint.
    /// </param>
    /// <returns>
    /// A <see cref="ScriptFingerprint"/> that uniquely represents the state of the specified script.
    /// </returns>
    public static ScriptFingerprint Create(Script script)
    {
        return Create(
            script.Code,
            script.Config.Namespaces,
            script.Config.References,
            script.Config.Kind,
            script.Config.TargetFrameworkVersion,
            script.Config.OptimizationLevel,
            script.Config.UseAspNet,
            script.DataConnection?.Id
        );
    }

    /// <summary>
    /// Creates a new <see cref="ScriptFingerprint"/> from the specified script components.
    /// </summary>
    /// <param name="code">The script source code.</param>
    /// <param name="namespaces">The namespaces imported by the script.</param>
    /// <param name="references">The assembly or package references used by the script.</param>
    /// <param name="kind">The kind or execution type of the script.</param>
    /// <param name="targetFrameworkVersion">The .NET framework version targeted by the script.</param>
    /// <param name="optimizationLevel">The compiler optimization level.</param>
    /// <param name="useAspNet">Indicates whether the script uses ASP.NET features.</param>
    /// <param name="dataConnectionId">An optional identifier for the associated data connection.</param>
    /// <returns>
    /// A <see cref="ScriptFingerprint"/> that uniquely represents the provided script
    /// components and its configuration.
    /// </returns>
    /// <remarks>
    /// Each component is normalized and hashed using SHA-256 to produce a deterministic fingerprint
    /// that changes whenever any of the script’s relevant inputs change.
    /// </remarks>
    public static ScriptFingerprint Create(
        string code,
        IEnumerable<string> namespaces,
        IEnumerable<Reference> references,
        ScriptKind kind,
        DotNetFrameworkVersion targetFrameworkVersion,
        OptimizationLevel optimizationLevel,
        bool useAspNet,
        Guid? dataConnectionId
    )
    {
        var codeHash = HashCode(code);
        var nsHash = HashNamespaces(namespaces);
        var refHash = HashReferences(references);

        return new ScriptFingerprint(
            AppVersion: AppIdentifier.PRODUCT_VERSION,
            CodeHash: codeHash,
            NamespacesHash: nsHash,
            ReferencesHash: refHash,
            Kind: kind,
            TargetFrameworkVersion: targetFrameworkVersion,
            OptimizationLevel: optimizationLevel,
            UseAspNet: useAspNet,
            DataConnectionId: dataConnectionId
        );
    }

    private static string HashCode(string code) => Sha256Hex(code);

    private static string HashNamespaces(IEnumerable<string> namespaces)
    {
        var list = namespaces
            .Select(ns => ns.Trim())
            .Where(ns => ns.Length > 0)
            // Deduplicate and make order-insensitive
            .Distinct(StringComparer.Ordinal)
            .OrderBy(ns => ns, StringComparer.Ordinal)
            .ToArray();

        var joined = string.Join("\n", list);
        return Sha256Hex(joined);
    }

    private static string HashReferences(IEnumerable<Reference> references)
    {
        var normalized = references
            .Select(NormalizeReference)
            // Deduplicate and make order-insensitive
            .Distinct(StringComparer.Ordinal)
            .OrderBy(s => s, StringComparer.Ordinal);

        var joined = string.Join(string.Empty, normalized);
        return Sha256Hex(joined);

        static string NormalizeReference(Reference r)
        {
            return r switch
            {
                PackageReference packageReference => $"{packageReference.PackageId}{packageReference.Version}",
                AssemblyFileReference assemblyFileReference => assemblyFileReference.AssemblyPath,
                AssemblyImageReference assemblyImageReference =>
                    $"{assemblyImageReference.AssemblyImage.Image.LongLength}{assemblyImageReference.AssemblyImage.AssemblyName.FullName}",
                _ => throw new NotSupportedException($"{r.GetType().Name} is not supported.")
            };
        }
    }

    private static string Sha256Hex(string s)
    {
        var bytes = Encoding.UTF8.GetBytes(s);
        var hash = SHA256.HashData(bytes);
        var sb = new StringBuilder(hash.Length * 2);
        foreach (var b in hash) sb.Append(b.ToString("x2"));
        return sb.ToString();
    }
}
